﻿using System;
using System.Collections.Generic;
using System.Text;
using UnityEngine;



public class DungeonPartition
{
    public enum SplitType
    {
        None,
        Horizontal,
        Vertical
    }

    private static int nextId = 0;
    public static System.Random randomizer = new System.Random();

    public int ID { get; protected set; }

    public DungeonPartition Left { get; protected set; }
    public DungeonPartition Right { get; protected set; }
    public DungeonPartition Parent { get; protected set; }
    public Dungeon Container { get; protected set; }

    public bool IsLeaf { get { return Left == null && Right == null; } }

    public int Width { get; protected set; }
    public int Height { get; protected set; }
    public int X { get; protected set; }
    public int Y { get; protected set; }

    public SplitType Split { get; protected set; }

    public Room Room { get; protected set; }

    public Corridor Corridor { get; set; }

    public bool ChildrenAreLeafs
    {
        get
        {
            if (Left == null) return false;
            if (Right == null) return false;

            return Left.IsLeaf && Right.IsLeaf;

        }
    }

    protected DungeonPartition() { }

    public DungeonPartition(DungeonPartition parent, Dungeon container)
    {
        ID = nextId++;
        Left = null;
        Right = null;
        this.Parent = parent;
        this.Container = container;

        if (parent == null)
        {
            Width = container.Size;
            Height = container.Size;
        }
    }

    public int Partition()
    {
        if (!IsLeaf)
        {
            int totalPartitions = 0;
            totalPartitions = Left.Partition();
            totalPartitions += Right.Partition();
            return totalPartitions;
        }

        SplitType splitType = DungeonPartition.SplitType.None;
        if (Parent != null)
        {
            if (Parent.Split == SplitType.Horizontal)
                splitType = SplitType.Vertical;
            else
                splitType = SplitType.Horizontal;
        }
        else
        {
            splitType = randomizer.Next(0, 100) < 50 ? SplitType.Horizontal : SplitType.Vertical;
            //splitType = SplitType.Vertical;
        }

        // TODO: Create two new partitions within this space
        // according to the split type, after calculating
        // a good location for the split itself
        switch (splitType)
        {
            case SplitType.Horizontal:
                HorizontalPartition();
                this.Split = SplitType.Horizontal;
                break;
            case SplitType.Vertical:
                VerticalPartition();
                this.Split = SplitType.Vertical;
                break;
        }

        return 1;
    }

    /// <summary>
    /// Create a left and right partition by vertically dividing the space.
    /// </summary>
    private void VerticalPartition()
    {
        // If the space is N squares wide, there are N-1 spots to do the splitting.
        // We want our spaces to be a certain minimum width.
        // Our division is an integer, that being the square where our line first appears.
        // Thus a split at position 1 will leave an upper area 1 space high.

        // So, our split possibilities with minimum width 1 are [1.N].
        // Our split possibilities with minimum width 2 are thus [2.N-1].

        int trueMinSplit = 0;
        int trueMaxSplit = 0;

        if (!ClampRange(this.Width, Container.MinPartitionDimensions, this.Width - Container.MinPartitionDimensions, Container.MinSplitRatio, Container.MaxSplitRatio, out trueMinSplit, out trueMaxSplit)) 
        {
            return;
        }

        if (trueMinSplit <= trueMaxSplit)
        {
            int trueSplitLocation = randomizer.Next(trueMinSplit, trueMaxSplit);

            Left = new DungeonPartition(this, this.Container);
            Left.X = X;
            Left.Y = Y;
            Left.Height = Height;
            Left.Width = trueSplitLocation;

            Right = new DungeonPartition(this, this.Container);
            Right.X = X + trueSplitLocation;
            Right.Y = Y;
            Right.Height = Height;
            Right.Width = Width - trueSplitLocation;
        }
    }

    /// <summary>
    /// Create a top and bottom partition by horizontally dividing the space.
    /// </summary>
    private void HorizontalPartition()
    {
        int trueMinSplit = 0;
        int trueMaxSplit = 0;

        if (!ClampRange(this.Height, Container.MinPartitionDimensions, this.Height - Container.MinPartitionDimensions, Container.MinSplitRatio, Container.MaxSplitRatio, out trueMinSplit, out trueMaxSplit))
        {
            return;
        }

        if (trueMinSplit <= trueMaxSplit)
        {
            int trueSplitLocation = randomizer.Next(trueMinSplit, trueMaxSplit);

            Left = new DungeonPartition(this, this.Container);
            Left.X = X;
            Left.Y = Y;
            Left.Height = trueSplitLocation;
            Left.Width = Width;

            Right = new DungeonPartition(this, this.Container);
            Right.X = X;
            Right.Y = Y + trueSplitLocation;
            Right.Height = Height - trueSplitLocation;
            Right.Width = Width;
        }
    }

    public void AppendWhenChildrenAreLeaves(List<DungeonPartition> validPartitions)
    {
        if (ChildrenAreLeafs)
            validPartitions.Add(this);
        else
        {
            if (!IsLeaf)
            {
                Left.AppendWhenChildrenAreLeaves(validPartitions);
                Right.AppendWhenChildrenAreLeaves(validPartitions);
            }
        }
    }


    internal void AppendWhenChildrenAreNotLeaves(List<DungeonPartition> partitions)
    {
        if (IsLeaf) return;
        if (ChildrenAreLeafs) return;

        partitions.Add(this);
        Left.AppendWhenChildrenAreNotLeaves(partitions);
        Right.AppendWhenChildrenAreNotLeaves(partitions);

    }

    internal void GetCorridors(List<Corridor> corridors)
    {
        if (this.Corridor != null) corridors.Add(this.Corridor);
        if (!IsLeaf)
        {
            Left.GetCorridors(corridors);
            Right.GetCorridors(corridors);
        }
    }

    internal void GenerateRooms(List<Room> rooms)
    {
        if (IsLeaf)
        {
            //UnityEngine.MonoBehaviour.print("found leaf, creating room");
            int trueMinRoomWidth = 0;
            int trueMaxRoomWidth = 0;

            //UnityEngine.MonoBehaviour.print("Checking dimensions for width");
            if (!ClampRange(this.Width, Container.AbsoluteMinRoomDimension, this.Width - Container.MinRoomMargin * 2, Container.MinRoomDimensions, Container.MaxRoomDimensions, out trueMinRoomWidth, out trueMaxRoomWidth)) return;

            int trueMinRoomHeight = 0;
            int trueMaxRoomHeight = 0;

            //UnityEngine.MonoBehaviour.print("Checking dimensions for height");
            if (!ClampRange(this.Height, Container.AbsoluteMinRoomDimension, this.Height - Container.MinRoomMargin * 2, Container.MinRoomDimensions, Container.MaxRoomDimensions, out trueMinRoomHeight, out trueMaxRoomHeight)) return;

            int roomWidth = randomizer.Next(trueMinRoomWidth, trueMaxRoomWidth);
            int roomHeight = randomizer.Next(trueMinRoomHeight, trueMaxRoomHeight);

            // x is between Container.MinRoomMargin and this.Width - roomWidth - Container.MinRoomMargin. Right?
            int roomX = this.X + randomizer.Next(Container.MinRoomMargin, this.Width - roomWidth - Container.MinRoomMargin);
            int roomY = this.Y + randomizer.Next(Container.MinRoomMargin, this.Height - roomHeight - Container.MinRoomMargin);

            this.Room = new Room(roomX, roomY, roomWidth, roomHeight);
            rooms.Add(Room);
        }
        else
        {
            if (Left != null) Left.GenerateRooms(rooms);
            if (Right != null) Right.GenerateRooms(rooms);
        }

    }

    /// <summary>
    /// Clamps an integer range based on its restrictions.
    /// </summary>
    /// <param name="totalRange">The total range (such as the width or height of the partition).</param>
    /// <param name="absoluteMin">The absolute minimum value for the range as defined by margins.</param>
    /// <param name="absoluteMax">The absolute maximum value for the range as defined by margins.</param>
    /// <param name="minRatio">The minimum ratio where the value can appear.</param>
    /// <param name="maxRatio">The maximum ratio where the value can appear.</param>
    /// <param name="trueMin">The true minimum value for the range.</param>
    /// <param name="trueMax">The true maximum value for the range.</param>
    /// <returns>True if there is a valid range, false otherwise.</returns>
    private bool ClampRange(int totalRange, int absoluteMin, int absoluteMax, float minRatio, float maxRatio, out int trueMin, out int trueMax) 
    {
        //UnityEngine.MonoBehaviour.print(string.Format("ClampRange({0}, {1}, {2}, {3}, {4})", totalRange, absoluteMin, absoluteMax, minRatio, maxRatio));

        // Initialize output ahead of time.
        trueMin = absoluteMin;
        trueMax = absoluteMax;

        // Calculate the restricted values based on the ratios provided.
        int restrictedMin = (int)System.Math.Floor(totalRange * minRatio);
        int restrictedMax = (int)System.Math.Floor(totalRange * maxRatio);

        // If the restricted values cannot fit within the absolute values for some reason,
        // there is no available range and we return false.
        if (restrictedMin > absoluteMax) return false;
        if (restrictedMax < absoluteMin) return false;

        trueMin = absoluteMin;
        if (restrictedMin > absoluteMin)
            trueMin = restrictedMin;

        trueMax = absoluteMax;
        if (restrictedMax < absoluteMax)
            trueMax = restrictedMax;

        return true;
    }

}

